package org.hawk.gwt.ppc.scanner.utils;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.reflect.emul.java.lang.NoClassDefFoundError;
import org.hawk.gwt.ppc.reflect.lang.ClassDelegate;
import org.hawk.gwt.ppc.reflect.lang.LangClassLoader;
import org.hawk.gwt.ppc.reflect.lang.SystemClassLoader;
import org.hawk.gwt.ppc.scanner.SourcesLoader;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.StringUtils;
/**
 * Represents class-level java scope.
 * @author alex.bereznevatiy@gmail.com
 */
public class JavaTypeScope extends JavaScope implements JavaNameResolver {
	private static final Map<String,String> wrappers;
	static{
		Map<String,String> m = new HashMap<String, String>();
		for(Class<?> cls : ClassDelegate.WRAPPERS.keySet()){
			m.put(cls.getName(), ClassDelegate.WRAPPERS.get(cls).getName());
		}
		wrappers = Collections.unmodifiableMap(m);
	}
	
	private static final Map<String,String> unwrappers;
	static{
		Map<String,String> m = new HashMap<String, String>();
		for(Class<?> cls : ClassDelegate.WRAPPERS.keySet()){
			m.put(ClassDelegate.WRAPPERS.get(cls).getName(),cls.getName());
		}
		unwrappers = Collections.unmodifiableMap(m);
	}

	protected final SourcesLoader loader;
	
	private Map<String,String> imports;
	private String currentPackage;
	private String superClass;
	private List<String> implementedInterfaces;
	private List<JavaConstructor> constructors;
	private String className;
	private String entityType;
	private List<JavaTypeVariable> typeParameters;
	private boolean anonymous;

	JavaTypeScope(JavaScope parent, int start, 
			List<JavaVariable> vars,SourcesLoader loader) {
		super(parent, start, vars);
		this.imports = new HashMap<String, String>();
		this.loader = loader;
		this.implementedInterfaces = new LinkedList<String>();
		this.constructors = new LinkedList<JavaConstructor>();
		this.typeParameters = new LinkedList<JavaTypeVariable>();
	}
	
	void setCurrentPackage(String currentPackage){
		this.currentPackage = currentPackage;
	}

	void addImport(String name) {
		imports.put(Name.getSimpleName(name), name);
	}
	
	void addTypeParameter(String name, List<String> bounds) throws IOException, ParseException {
		typeParameters.add(new JavaTypeVariable(name, this, this, bounds));
	}
	
	void setExtendedClass(String name){
		if(name.indexOf('.') < 0){
			name = resolveType(name);
		}
		superClass = name;
	}
	
	void setClassName(String name){
		className = name;
	}
	/**
	 * @return name of the type this scope is representing.
	 */
	public String getClassName(){
		return className;
	}
	/**
	 * @return resolved name of the type this scope is representing.
	 * Name of the package will be included in the presentation.
	 */
	public String getResolvedClassName(){
		return Name.getChildJavaName(currentPackage, className);
	}
	
	void addImplementedInterface(String name){
		implementedInterfaces.add(name);
	}

	void setEntityType(String entityType) {
		this.entityType = entityType.intern();
	}
	/**
	 * @return keyword that represents entity type
	 * of this entity. The valid results are:
	 * <ul>
	 * <li>class
	 * <li>interface
	 * <li>enum
	 * <li>@interface
	 * </ul>
	 */
	public String getEntityType(){
		if(entityType==null){
			return JavaClassDescriptor.CLASS_KEYWORD;
		}
		return entityType;
	}
	/**
	 * @return <code>true</code> if this type is an interface
	 */
	public boolean isInterface(){
		return getEntityType().equals(JavaClassDescriptor.INTERFACE_KEYWORD);
	}
	/**
	 * @return <code>true</code> if this type is a class
	 */
	public boolean isClass(){
		return getEntityType().equals(JavaClassDescriptor.CLASS_KEYWORD);
	}
	/**
	 * @return <code>true</code> if this type is enum
	 */
	public boolean isEnum(){
		return getEntityType().equals(JavaClassDescriptor.ENUM_KEYWORD);
	}
	/**
	 * @return <code>true</code> if this type is annotation
	 */
	public boolean isAnnotation(){
		return getEntityType().equals(JavaClassDescriptor.ANNOTATION_KEYWORD_START);
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.utils.JavaScope#getVariables(int)
	 */
	@Override
	public List<JavaVariable> getVariables(int index) {
		List<JavaVariable> result = new LinkedList<JavaVariable>();
		result.addAll(getSuperClass().getVariables(index));
		for(JavaTypeScope iface : getImplementedInterfaces()){
			result.addAll(iface.getVariables(index));
		}
		if(getParent()!=null){
			result.addAll(getParent().getVariables(index));
		}
		for(JavaVariable var : vars){
			result.add(var);
		}
		return Collections.unmodifiableList(result);
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.utils.JavaScope#getScope(int)
	 */
	@Override
	public JavaScope getScope(int index) {
		if (index < 0) {
			return this;
		}
		return super.getScope(index);
	}

	/**
	 * @return list of the type scopes for the implemented interfaces.
	 */
	public List<JavaTypeScope> getImplementedInterfaces() {
		if (isAnnotation()) {
			return Collections.singletonList(JavaClassDescriptorUtils.getClassDescriptor(Annotation.class).getCompilationUnit());
		}
		List<JavaTypeScope> result = new LinkedList<JavaTypeScope>();
		for(String iface : implementedInterfaces){
			result.add(resolveTypeDescriptor(iface).getCompilationUnit());
		}
		return Collections.unmodifiableList(result);
	}
	
	void addConstructor(JavaConstructor c) {
		constructors.add(c);
	}
	/**
	 * @return list of fields declared by this type
	 */
	public List<JavaVariable> getFields() {
		return Collections.unmodifiableList(vars);
	}
	/**
	 * @return list of declared constructors.
	 */
	public List<JavaConstructor> getDeclaredConstructors() {
		if (isInterface() || isAnnotation()) {
			return Collections.emptyList();
		}
		if (constructors.isEmpty()) {
			JavaConstructor defaultConstructor = new JavaConstructor(className, this, -1, new LinkedList<JavaVariable>(), false);
			defaultConstructor.setModifiers(Collections.singletonList(JavaModifier.PUBLIC));
			return Collections.singletonList(defaultConstructor);
		}
		return Collections.unmodifiableList(constructors);
	}
	
	public List<JavaTypeVariable> getTypeVariables() {
		return Collections.unmodifiableList(typeParameters);
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.utils.JavaNameResolver#resolveTypeDescriptor(java.lang.String)
	 */
	public JavaClassDescriptor resolveTypeDescriptor(final String type) {
		String typeName = resolveType(type);
		try {
			return JavaClassDescriptorUtils.getClassDescriptor(typeName, loader);
		}
		catch(Exception ex){}
		
		String possibleAnonimousType = getPossibleAnonimousName(typeName);
		if (possibleAnonimousType != null) {
			try {
				return resolveTypeDescriptor(possibleAnonimousType);
			} catch (Throwable th) {
			}
		}
		
		possibleAnonimousType = getPossibleAnonimousName(type);
		if (possibleAnonimousType != null) {
			try {
				return resolveTypeDescriptor(possibleAnonimousType);
			} catch (Throwable th) {
			}
		}
		
		try {
			return new JavaClassDescriptor(findClass(typeName));
		}
		catch(Throwable ex){
		}
		
		// Don't give up - try every anonymous class you may got:
		
		while (Name.isFullName(possibleAnonimousType)) {
			possibleAnonimousType = StringUtils.replaceLast(possibleAnonimousType, ".", "$");
			try {
				return resolveTypeDescriptor(possibleAnonimousType);
			} catch (Throwable th) {
			}
		}
		
		possibleAnonimousType = getPossibleAnonimousName(typeName);
		
		while (Name.isFullName(possibleAnonimousType)) {
			possibleAnonimousType = StringUtils.replaceLast(possibleAnonimousType, ".", "$");
			try {
				return resolveTypeDescriptor(possibleAnonimousType);
			} catch (Throwable th) {
			}
		}
		
		throw new NoClassDefFoundError("Unable to find the class with name " + type + 
				". Please ensure that both binary and sources are available in the classpath");
		// TODO: need to change all such a errors to good runtime exceptions specific for preprocessor only
	}

	private String getPossibleAnonimousName(String type) {
		if (type.lastIndexOf(Name.PACKAGE_SEPARATOR) < type.lastIndexOf(Name.ANONIMOUS_TYPE_SEPARATOR)) {
			return null;
		}
		if (!Name.isFullName(type)) {
			return getResolvedClassName() + Name.ANONIMOUS_TYPE_SEPARATOR + type;
		}
		int index = type.lastIndexOf(Name.PACKAGE_SEPARATOR);
		
		return resolveType(type.substring(0, index)) + Name.ANONIMOUS_TYPE_SEPARATOR + type.substring(index + 1);
	}

	private Class<?> findClass(String type) throws ClassNotFoundException {
		if(SystemClassLoader.isPrimitiveClass(type)){
			return SystemClassLoader.getPrimitiveClass(type);
		}
		if (type.endsWith("]")) {
			return Class.forName(SystemClassLoader.getArrayTypeName(type.substring(0, type.indexOf('['))));
		}
		return Class.forName(Name.getRawName(type));
	}

	/**
	 * @return supertype pf the type this type scope represents
	 */
	public JavaTypeScope getSuperClass() {
		if (superClass == null || resolveType(superClass).equals(Object.class.getName())) {
			return ObjectTypeScope.INSTANCE;
		}
		return resolveTypeDescriptor(superClass).getCompilationUnit();
	}
	/**
	 * @return JavaGenericTypeScope representing superclass of this class with corresponding
	 * parameters or <code>null</code> if superclass is not generic.
	 */
	public JavaGenericTypeScope getGenericSuperclass() {
		if (superClass == null) {
			return null;
		}
		String params = Name.getTypeParameters(superClass);
		if (params != null && !params.isEmpty()) {
			return new JavaParameterizedGenericTypeScope(resolveTypeDescriptor(superClass).getCompilationUnit(), getOwner(), params);
		}
		return null;
	}
	/**
	 * @return list of scopes representing generic interfaces of this type
	 * or <code>null</code> in case there aren't any.
	 */
	public List<JavaTypeScope> getGenericInterfaces() {
		Iterator<JavaTypeScope> ifaces = getImplementedInterfaces().iterator();
		List<JavaTypeScope> result = new ArrayList<JavaTypeScope>(implementedInterfaces.size());
		boolean returnNull = true;
		for(String iface : implementedInterfaces){
			String params = Name.getTypeParameters(iface);
			if (params != null && !params.isEmpty()) {
				returnNull = false;
				result.add( new JavaParameterizedGenericTypeScope(ifaces.next(), getOwner(), params));
			} else {
				result.add(ifaces.next());
			}
		}
		if (returnNull) {
			return null;
		}
		return result;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.utils.JavaScope#getVariable(int, java.lang.String)
	 */
	@Override
	public JavaVariable getVariable(int index, String name) {
		List<JavaVariable> vars = getVariables(index);
		ListIterator<JavaVariable> iterator = vars.listIterator(vars.size());
		while(iterator.hasPrevious()) {
			JavaVariable var = iterator.previous();
			if(var.getName().equals(name)) {
				return var;
			}
		}
		return null;
	}
	/**
	 * @return list of methods declared by this type and supertypes.
	 */
	public List<JavaMethod> getMethods() {
		List<JavaMethod> result = new ArrayList<JavaMethod>();
		result.addAll(getDeclaredMethods());
		result.addAll(getSuperClass().getMethods());
		for(JavaTypeScope iface : getImplementedInterfaces()){
			result.addAll(iface.getMethods());
		}
		return Collections.unmodifiableList(result);
	}
	/**
	 * @return list of methods declared within this type
	 */
	public List<JavaMethod> getDeclaredMethods() {
		List<JavaMethod> result = new ArrayList<JavaMethod>();
		for (JavaScope child : getChildren()) {
			if (child instanceof JavaMethod && !(child instanceof JavaConstructor)) {
				result.add((JavaMethod)child);
			}
		}
		return Collections.unmodifiableList(result);
	}
	/**
	 * @param name of the method to find
	 * @param parametersTypes names of parameter types of method to find.
	 * @return method matching name and parameters passed
	 */
	public JavaMethod getMethod(String name, String...parametersTypes) {
		for(JavaMethod m : getMethods()){
			if(m.matches(name,parametersTypes)){
				return m;
			}
		}
		JavaScope parent = getParent();
		while(parent!=null){
			if(parent instanceof JavaTypeScope){
				return ((JavaTypeScope)parent).getMethod(name, parametersTypes);
			}
			parent = getParent();
		}
		return null;
	}

	@Override
	public String resolveType(final String typeSimpleName) {
		String pureName = Name.getPureClassName(typeSimpleName);
		JavaTypeVariable var = resolveTypeParameter(pureName);
		if (var != null) {
			return ( var.getBounds().isEmpty() ? Object.class.getName() : var.getBounds().get(0).getResolvedClassName() )
					+ typeSimpleName.substring(pureName.length());
		}
		String rawName = Name.getRawName(typeSimpleName);
		String typeParams = Name.getTypeParameters(typeSimpleName);
		if (rawName.indexOf('.') >= 0) {
			String anon = resolveAnonimousType(rawName);
			return anon == null ? typeSimpleName : (anon + typeParams);
		}
		if (imports.get(rawName) != null) {
			return imports.get(rawName) + typeParams;
		}
		
		String pkgPrivateName = Name.getChildJavaName(currentPackage, rawName);
		if (loader != null && loader.isClassExists(pkgPrivateName)) {
			return pkgPrivateName + typeParams;
		}
		if (LangClassLoader.isLangClass(rawName)) {
			return Name.getChildJavaName(LangClassLoader.LANG_PACKAGE, typeSimpleName);
		}
		return super.resolveType(typeSimpleName);
	}

	private String resolveAnonimousType(final String rawName) {
		String superClass = Name.getParentJavaName(rawName);
		String superClassSimpleName = Name.getSimpleName(superClass);
		if (superClassSimpleName.length() > 0 && Character.isUpperCase(superClassSimpleName.charAt(0))) {
			return Name.getChildJavaName(resolveType(superClass), Name.getSimpleName(rawName));
		}
		return null;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof JavaTypeScope) {
			JavaTypeScope type = (JavaTypeScope)obj;
			return type.className.equals(className);
		}
		return super.equals(obj);
	}

	@Override
	public String toString() {
		return "["+getClass().getSimpleName()+']'+className;
	}

	public boolean isAssignableFrom(JavaTypeScope type) {
		JavaTypeScope compare = this;
		if (equals(type)) {
			return true;
		}
		while(compare != null && type != null) {
			if (type.equals(compare)) {
				return true;
			}
			compare = compare.getSuperClass();
		}
		if (wrappers.containsKey(className)) {
			return wrappers.get(className).equals(type.className);
		}
		if (unwrappers.containsKey(className)) {
			return unwrappers.get(className).equals(type.className);
		}
		return false;
	}
	/**
	 * @return owner of this class
	 */
	public JavaNameResolver getOwner() {
		return this;
	}
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.utils.JavaNameResolver#resolveTypeParameter(java.lang.String)
	 */
	public JavaTypeVariable resolveTypeParameter(String name) {
		for (JavaTypeVariable var : typeParameters) {
			if (var.getName().equals(name)) {
				return var;
			}
		}
		return null;
	}

	void markAnonymous() {
		anonymous = true;
	}
	
	public boolean isAnonymous() {
		return anonymous;
	}
}
